9.4 Список Задач + Dependency Inversion Principle
4 из 4 шагов пройдено

 Список Задач + Dependency Inversion Principle

➡️Ссылка на репозиторий с кодом этого урока

Создание контракта (Интерфейса)

Добавим новый файл для интерфейса. Он будет определять все публичные методы, которые должен иметь любой сервис хранения данных в приложении.

Файл lib/services/storage_interface.dart

import '../models/task.dart';  
  
// Префикс 'I' (IStorageService) - соглашение для именования интерфейсов.  
abstract interface class IStorageService {  
  Future<bool> getThemeMode();  
  Future<void> saveThemeMode(bool isDarkMode);  
  
  Future<List<Task>> getAllTasks();  
  Future<Task> createTask(String text);  
  Future<void> updateTask(Task task);  
  Future<void> deleteTask(int id); 
}

Теперь любой класс, который захочет выступать в роли сервиса хранения, должен будет реализовать (implements) этот интерфейс и предоставить свою версию каждого из этих четырёх методов.

Реализация контракта

Переименуем StorageService в SharedPreferencesService и заставим его "подписать" контракт с помощью ключевого слова implements.

Файл lib/services/shared_preferences_service.dart

import 'dart:convert';  
import 'package:shared_preferences/shared_preferences.dart';  
import 'package:todo_shared_preferences_2025/services/storage_interface.dart';  
import '../models/task.dart';  
  
  
class SharedPreferencesService implements IStorageService {  
  
  static const taskPrefix = 'todo_'; // префикс для ключей задач  
  static const themeKey = 'isDarkMode'; // ключ для цветовой темы  
  
  /// Сохранить значение цветовой темы  
  @override  
  Future<void> saveThemeMode(bool isDarkMode) async {  
    try {  
      final prefs = await SharedPreferences.getInstance();  
      await prefs.setBool(themeKey, isDarkMode);  
    } catch (e) {  
      throw Exception('Ошибка при сохранении темы: $e');  
    }  
  }  
  
  /// Получить значение сохранённой цветовой темы  
  @override  
  Future<bool> getThemeMode() async {  
    try {  
      final prefs = await SharedPreferences.getInstance();  
      // Возвращаем сохранённое значение или false, если ничего не найдено  
      return prefs.getBool(themeKey) ?? false;  
    } catch (e) {  
      throw Exception('Ошибка при загрузке темы: $e');  
    }  
  }  
  
  // МЕТОДЫ ДЛЯ РАБОТЫ СО СПИСКОМ ЗАДАЧ  
  
  /// Получить список всех задач  
  @override  
  Future<List<Task>> getAllTasks() async {  
    try {  
      final prefs = await SharedPreferences.getInstance();  
      // Получить все ключи из хранилища  
      final allKeys = prefs.getKeys();  
      // Найти ключи которые относятся к задачам  
      final taskKeys = allKeys.where((key) => key.startsWith(taskPrefix));  
  
      final List<Task> tasks = [];  
      // 1 Перебираем ключи задач в цикле  
      // 2 По ключу получаем значение задачи из хранилища
	  // 3 Декодируем JSON обратно в Map и создаём объект Task
	  // 4 Добавляем объект Task в список и потом возвращаем его
	  for (final key in taskKeys) {  
        final taskJsonString = prefs.getString(key);  
        if (taskJsonString != null) {  
          // Декодируем JSON обратно в Map  
          final taskMap = jsonDecode(taskJsonString);  
          // Создаем объект Task из Map и добавляем в список  
          tasks.add(Task.fromJson(taskMap));  
        }  
      }  
      return tasks;  
    } catch (e) {  
      throw Exception('Ошибка при загрузке всех задач: $e');  
    }  
  }  
  
  // Вспомогательный метод для генерации ключа по ID задачи  
  String _getTaskKey(int id) => '$taskPrefix$id';  
  
  /// Создать новую задачу  
  @override  
  Future<Task> createTask(String text) async {  
    try {  
      // Получаем список всех задач  
      final allTasks = await getAllTasks();  
      int maxId = 0;  
      // Находим максимальный ID  
      for (var task in allTasks) {  
        if (task.id > maxId) {  
          maxId = task.id;  
        }  
      }  
      // Генерируем новый ID, если список задач пуст то id = 1  
      final newId = allTasks.isNotEmpty ? maxId + 1 : 1;  
  
      // Создаем новую задачу с заданным ID  
      final newTask = Task(id: newId, text: text, isDone: false);  
  
      // Сохраняем новую задачу в хранилище  
      final prefs = await SharedPreferences.getInstance();  
      final key = _getTaskKey(newTask.id);  
      final taskJson = jsonEncode(newTask.toJson());  
      await prefs.setString(key, taskJson);  
  
      return newTask;  
    } catch (e) {  
      throw Exception('Ошибка при создании задачи: $e');  
    }  
  }  
  
  /// Обновить существующую задачу  
  @override  
  Future<void> updateTask(Task task) async {  
    try {  
      final prefs = await SharedPreferences.getInstance();  
      // Фомируем ключ для локального хранилища  
      final key = _getTaskKey(task.id);  
      // Кодируем обновлённый объект Task в JSON  
      final taskJson = jsonEncode(task.toJson());  
      // Перезаписываем значение по существующему ключу  
      await prefs.setString(key, taskJson);  
    } catch (e) {  
      throw Exception('Ошибка при обновлении задачи с ID ${task.id}: $e');  
    }  
  }  
  
  /// Удалить задачу по её ID  
  @override  
  Future<void> deleteTask(int id) async {  
    try {  
      final prefs = await SharedPreferences.getInstance();  
      final key = _getTaskKey(id);  
      await prefs.remove(key);  
    } catch (e) {  
      throw Exception('Ошибка при удалении задачи с ID $id: $e');  
    }  
  }  
}

Модификация ViewModel для Внедрения Зависимостей

Теперь самая важная часть. "Отвяжем" ToDoViewModel от конкретной реализации. Он будет требовать в своем конструкторе любой объект, который соответствует контракту IStorageService.

Файл lib/viewmodels/todo_viewmodel.dart

import 'package:flutter/material.dart';  
import '../models/task.dart';  
import '../services/storage_interface.dart';  
  
  
class ToDoViewModel extends ChangeNotifier {  
  
  // 👉 ViewModel больше не знает SharedPreferences это или SQLite.
  // 👉 Она знает только о контракте IStorageService.
  final IStorageService _storageService;  
  
  // Добавляем TextEditingController в ViewModel  
  final TextEditingController textEditingController = TextEditingController();  
  
  bool _isDarkMode = false;  
  bool get isDarkMode => _isDarkMode;  
  
  // Состояние для списка задач  
  List<Task> _tasks = [];  
  List<Task> get tasks => _tasks;  
  
  // 👉 ЗАВИСИМОСТЬ ИНЪЕКТИРУЕТСЯ ЗДЕСЬ через конструктор
  ToDoViewModel({required IStorageService storageService})  
      : _storageService = storageService {  
    loadTasks();  
  }  
  
  /// Переключение цветовой темы  
  void toggleTheme() {  
    // Изменяем состояние  
    _isDarkMode = !_isDarkMode;  
    // Сохраняем значение в SharedPreferences  
    _storageService.saveThemeMode(_isDarkMode);  
    // Уведомляем слушателей, чтобы они перестраивались  
    notifyListeners();  
  }  
  
  /// Загрузить все задачи из сервиса  
  Future<void> loadTasks() async {  
    _tasks = await _storageService.getAllTasks();  
    _isDarkMode = await _storageService.getThemeMode();  
    // Уведомляем UI, что данные изменились и нужно перерисоваться  
    notifyListeners();  
  }  
  
  /// Добавить новую задачу.  
  Future<void> addTask(String text) async {  
    if (text.isEmpty) return;  
  
    // Создаём новую задачу, сохраняем в хранилище  
    final newTask = await _storageService.createTask(text);  
    // Добавляем в список _tasks для отображения в UI  
    _tasks.add(newTask);  
    // Уведомляем UI, что данные изменились и нужно перерисоваться  
    notifyListeners();  
  }  
  
  /// Обновить статус выполнения задачи  
  Future<void> updateTaskStatus(int id, bool isDone) async {  
    final taskIndex = _tasks.indexWhere((task) => task.id == id);  
  
    // Создать копию задачи с обновленным статусом  
    final updatedTask = _tasks[taskIndex].copyWith(isDone: isDone);  
  
    _tasks[taskIndex] = updatedTask;  
    notifyListeners();  
  
    await _storageService.updateTask(updatedTask);  
  }  
  
  /// Удалить задачу по ID.  
  Future<void> deleteTask(int id) async {  
    _tasks.removeWhere((task) => task.id == id);  
    notifyListeners();  
  
    // Удаляем из хранилища  
    await _storageService.deleteTask(id);  
  }  
}

Будьте вежливы и соблюдайте наши принципы сообщества. Пожалуйста, не оставляйте решения и подсказки в комментариях, для этого есть отдельный форум.
Оставить комментарий